#pragma once

namespace cc {
namespace geometry {

constexpr auto LOOK_FORWARD = 3;

/**
 * @en
 * A key frame in the curve.
 * @zh
 * 曲线中的一个关键帧。
 */
struct Keyframe {
    /**
     * @en Current frame time.
     * @zh 当前帧时间。
     */
    float time = 0;

    /**
     * @en Current frame value.
     * @zh 当前帧的值。
     */
    float value = 0;

    /**
     * @en In tangent value.
     * @zh 左切线。
     */
    float inTangent = 0;

    /**
     * @en Out tangent value.
     * @zh 右切线。
     */
    float outTangent = 0;
};

float evalOptCurve(float t, const ccstd::vector<float> &coefs);

struct OptimizedKey {
    float                index;
    float                time;
    float                endTime;
    ccstd::vector<float> coefficient;
    OptimizedKey() {
        index   = -1;
        time    = 0;
        endTime = 0;
        coefficient.resize(4);
    }
    static float evaluate(float t);
};

/**
 * @en
 * Describe a curve in which three times Hermite interpolation is used for each adjacent key frame.
 * @zh
 * 描述一条曲线，其中每个相邻关键帧采用三次hermite插值计算。
 */
class AnimationCurve {
    // @serializable
private:
    // _curve ! : RealCurve;

public:
    static ccstd::vector<KeyFrame> defaultKF = [ {
                                                    time : 0,
                                                    value : 1,
                                                    inTangent : 0,
                                                    outTangent : 0,
                                                },
                                                 {
                                                     time : 1,
                                                     value : 1,
                                                     inTangent : 0,
                                                     outTangent : 0,
                                                 } ];

    /**
     * For internal usage only.
     * @internal
     */
    get _internalCurve() {
        return this._curve;
    }

    /**
     * @en
     * The key frame of the curve.
     * @zh
     * 曲线的关键帧。
     */
    auto getKeyFrames() {
        return Array.from(this._curve.keyframes()).map(([ time, value ]) = > {
            const legacyKeyframe      = new Keyframe();
            legacyKeyframe.time       = time;
            legacyKeyframe.value      = value.value;
            legacyKeyframe.inTangent  = value.leftTangent;
            legacyKeyframe.outTangent = value.rightTangent;
            return legacyKeyframe;
        });
    }

    void setKeyFrames(value) {
        this._curve.assignSorted(value.map((legacyCurve) = > [
            legacyCurve.time,
            {
                interpolationMode : RealInterpolationMode.CUBIC,
                value : legacyCurve.value,
                leftTangent : legacyCurve.inTangent,
                rightTangent : legacyCurve.outTangent,
            },
        ]));
    }

    /**
     * @en
     * Loop mode [[WrapMode]] when the sampling time exceeds the left end.
     * @zh
     * 当采样时间超出左端时采用的循环模式[[WrapMode]]。
     */
    auto getPreWrapMode() {
        return toLegacyWrapMode(this._curve.preExtrapolation);
    }

    void sPreWrapMode(value) {
        this._curve.preExtrapolation = fromLegacyWrapMode(value);
    }

    /**
     * @en
     * Cycle mode [[WrapMode]] when the sampling time exceeds the right end.
     * @zh
     * 当采样时间超出右端时采用的循环模式[[WrapMode]]。
     */
    get postWrapMode() {
        return toLegacyWrapMode(this._curve.postExtrapolation);
    }

    set postWrapMode(value) {
        this._curve.postExtrapolation = fromLegacyWrapMode(value);
    }

private
    cachedKey : OptimizedKey;

    /**
     * 构造函数。
     * @param keyFrames 关键帧。
     */
    constructor(keyFrames
                : Keyframe[] | null | RealCurve = null) {
        if (keyFrames instanceof RealCurve) {
            this._curve = keyFrames;
        } else {
            const curve             = new RealCurve();
            this._curve             = curve;
            curve.preExtrapolation  = ExtrapolationMode.LOOP;
            curve.postExtrapolation = ExtrapolationMode.CLAMP;
            if (!keyFrames) {
                curve.assignSorted([
                    [ 0.0, {interpolationMode : RealInterpolationMode.CUBIC, value : 1.0} ],
                    [ 1.0, {interpolationMode : RealInterpolationMode.CUBIC, value : 1.0} ],
                ]);
            } else {
                curve.assignSorted(keyFrames.map((legacyKeyframe) = > [ legacyKeyframe.time, {
                                                                           interpolationMode : RealInterpolationMode.CUBIC,
                                                                           value : legacyKeyframe.value,
                                                                           leftTangent : legacyKeyframe.inTangent,
                                                                           rightTangent : legacyKeyframe.outTangent,
                                                                       } ]));
            }
        }
        this.cachedKey = new OptimizedKey();
    }

    /**
     * @en
     * Add a keyframe.
     * @zh
     * 添加一个关键帧。
     * @param keyFrame 关键帧。
     */
public
    addKey(keyFrame
           : Keyframe | null) {
        if (!keyFrame) {
            this._curve.clear();
        } else {
            this._curve.addKeyFrame(keyFrame.time, {
                interpolationMode : RealInterpolationMode.CUBIC,
                value : keyFrame.value,
                leftTangent : keyFrame.inTangent,
                rightTangent : keyFrame.outTangent,
            });
        }
    }

    /**
     * @ignore
     * @param time
     */
public
    evaluate_slow(time
                  : number) {
        return this._curve.evaluate(time);
    }

    /**
     * @en
     * Calculate the curve interpolation at a given point in time.
     * @zh
     * 计算给定时间点的曲线插值。
     * @param time 时间。
     */
public
    evaluate(time
             : number) {
        const {cachedKey, _curve : curve} = this;
        const nKeyframes                  = curve.keyFramesCount;
        const lastKeyframeIndex           = nKeyframes - 1;
        let wrappedTime                   = time;
        const extrapolationMode           = time < 0 ? curve.preExtrapolation : curve.postExtrapolation;
        const startTime                   = curve.getKeyframeTime(0);
        const endTime                     = curve.getKeyframeTime(lastKeyframeIndex);
        switch (extrapolationMode) {
            case ExtrapolationMode.LOOP:
                wrappedTime = repeat(time - startTime, endTime - startTime) + startTime;
                break;
            case ExtrapolationMode.PING_PONG:
                wrappedTime = pingPong(time - startTime, endTime - startTime) + startTime;
                break;
            case ExtrapolationMode.CLAMP:
            default:
                wrappedTime = clamp(time, startTime, endTime);
                break;
        }
        if (wrappedTime >= cachedKey.time && wrappedTime < cachedKey.endTime) {
            return cachedKey.evaluate(wrappedTime);
        }
        const leftIndex  = this.findIndex(cachedKey, wrappedTime);
        const rightIndex = Math.min(leftIndex + 1, lastKeyframeIndex);
        this.calcOptimizedKey(cachedKey, leftIndex, rightIndex);
        return cachedKey.evaluate(wrappedTime);
    }

    /**
     * @ignore
     * @param optKey
     * @param leftIndex
     * @param rightIndex
     */
public
    calcOptimizedKey(optKey
                     : OptimizedKey, leftIndex
                     : number, rightIndex
                     : number) {
        const lhsTime                                         = this._curve.getKeyframeTime(leftIndex);
        const rhsTime                                         = this._curve.getKeyframeTime(rightIndex);
        const {value : lhsValue, leftTangent : lhsOutTangent} = this._curve.getKeyframeValue(leftIndex);
        const {value : rhsValue, rightTangent : rhsInTangent} = this._curve.getKeyframeValue(rightIndex);
        optKey.index                                          = leftIndex;
        optKey.time                                           = lhsTime;
        optKey.endTime                                        = rhsTime;

        const dx     = rhsTime - lhsTime;
        const dy     = rhsValue - lhsValue;
        const length = 1 / (dx * dx);
        const d1     = lhsOutTangent * dx;
        const d2     = rhsInTangent * dx;

        optKey.coefficient[0] = (d1 + d2 - dy - dy) * length / dx;
        optKey.coefficient[1] = (dy + dy + dy - d1 - d1 - d2) * length;
        optKey.coefficient[2] = lhsOutTangent;
        optKey.coefficient[3] = lhsValue;
    }

    /**
     * @ignore
     * @param optKey
     * @param t
     */
private
    findIndex(optKey
              : OptimizedKey, t
              : number) {
        const {_curve : curve} = this;
        const nKeyframes       = curve.keyFramesCount;
        const cachedIndex      = optKey.index;
        if (cachedIndex != = -1) {
            const cachedTime = curve.getKeyframeTime(cachedIndex);
            if (t > cachedTime) {
                for (let i = 0; i < LOOK_FORWARD; i++) {
                    const currIndex = cachedIndex + i;
                    if (currIndex + 1 < nKeyframes && curve.getKeyframeTime(currIndex + 1) > t) {
                        return currIndex;
                    }
                }
            } else {
                for (let i = 0; i < LOOK_FORWARD; i++) {
                    const currIndex = cachedIndex - i;
                    if (currIndex >= 0 && curve.getKeyframeTime(currIndex - 1) <= t) {
                        return currIndex - 1;
                    }
                }
            }
        }
        let left  = 0;
        let right = nKeyframes;
        let mid;
        while (right - left > 1) {
            mid = Math.floor((left + right) / 2);
            if (curve.getKeyframeTime(mid) >= t) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return left;
    }
}

function
fromLegacyWrapMode(legacyWrapMode
                   : WrapModeMask) : ExtrapolationMode {
    switch (legacyWrapMode) {
        default:
        case WrapModeMask.Default:
        case WrapModeMask.Normal:
        case WrapModeMask.Clamp: return ExtrapolationMode.CLAMP;
        case WrapModeMask.PingPong: return ExtrapolationMode.PING_PONG;
        case WrapModeMask.Loop: return ExtrapolationMode.LOOP;
    }
}

function toLegacyWrapMode(extrapolationMode
                          : ExtrapolationMode) : WrapModeMask {
    switch (extrapolationMode) {
        default:
        case ExtrapolationMode.LINEAR:
        case ExtrapolationMode.CLAMP: return WrapModeMask.Clamp;
        case ExtrapolationMode.PING_PONG: return WrapModeMask.PingPong;
        case ExtrapolationMode.LOOP: return WrapModeMask.Loop;
    }
}

/**
 * Same as but more effective than `new LegacyCurve()._internalCurve`.
 */
export function constructLegacyCurveAndConvert() {
    const curve = new RealCurve();
    curve.assignSorted([
        [ 0.0, {interpolationMode : RealInterpolationMode.CUBIC, value : 1.0} ],
        [ 1.0, {interpolationMode : RealInterpolationMode.CUBIC, value : 1.0} ],
    ]);
    return curve;
}

}
} // namespace cc